/*
 * asmix is the AfterStep sound volume control knob for X Windows.
 * Copyright (c) 1998 original author unknown
 * Copyright (c) 1998-2004  Albert "Tigr" Dorofeev <albert@tigr.net>
 * Copyright (c) 2000  John "wizgrav" Gravezas <wizgrav@netsmart.gr>
 *
 * This software is distributed under GPL. For details see LICENSE file.
 */

#include "config.h"
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

#include <X11/Xlib.h>
#include <X11/xpm.h>
#include <X11/extensions/shape.h>
#include <time.h>
#include <math.h>
#include <X11/Xatom.h>
#include <sys/errno.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#ifdef HAVE_SYS_SOUNDCARD_H
#  include <sys/soundcard.h>
#endif
#ifdef HAVE_LINUX_SOUNDCARD_H
#  include <linux/soundcard.h>
#endif

typedef struct mixer_channel_ident_t
{
  char * channel_name;
  int    channel_id;
} mixer_channel_ident;

mixer_channel_ident channel_idents[] =
  {
    { "volume",    SOUND_MIXER_VOLUME	},
    { "bass",      SOUND_MIXER_BASS	},
    { "treble",    SOUND_MIXER_TREBLE	},
    { "synth",     SOUND_MIXER_SYNTH	},
    { "pcm",       SOUND_MIXER_PCM	},
    { "speaker",   SOUND_MIXER_SPEAKER	},
    { "line",      SOUND_MIXER_LINE	},
    { "mic",       SOUND_MIXER_MIC	},
    { "cd",        SOUND_MIXER_CD	},
    { "imix",      SOUND_MIXER_IMIX	},
    { "pcm2",      SOUND_MIXER_ALTPCM	},
    { "record",    SOUND_MIXER_RECLEV	},
    { "igain",     SOUND_MIXER_IGAIN	},
    { "ogain",     SOUND_MIXER_OGAIN	},
    { "line1",     SOUND_MIXER_LINE1	},
    { "line2",     SOUND_MIXER_LINE2	},
    { "line3",     SOUND_MIXER_LINE3	},
    { "digital1",  SOUND_MIXER_DIGITAL1	},
    { "digital2",  SOUND_MIXER_DIGITAL2	},
    { "digital3",  SOUND_MIXER_DIGITAL3	},
    { "phone-in",  SOUND_MIXER_PHONEIN	},
    { "phone-out", SOUND_MIXER_PHONEOUT	},
    { "video",     SOUND_MIXER_VIDEO	},
    { "radio",     SOUND_MIXER_RADIO	},
    { "monitor",   SOUND_MIXER_MONITOR	},
    { NULL, 0 }
  };

typedef struct stereovolume
{
  unsigned char left;
  unsigned char right;
  
} StereoVolume;


typedef struct volctrl
{
  int mixer_id;
  StereoVolume volume;
  int supported;
} VolumeControl;
 
VolumeControl Master;
int mixer_fd;

#include "volume.xpm"
/*#include "mask2.xbm"*/
#include "mark.xpm"

#define Center_X 24
#define Center_Y 26
#define Radius   13
#define min_a    0.75*M_PI
#define max_a    0.25*M_PI
#define WIZ     M_PI/8

int ONLYSHAPE=0;
int ICONIFIED=0; /* default is not iconified */
int WITHDRAWN=0; /* default is not withdrawn */


/* X11 Variablen *************************************************************/
Display *dpy;	  /* welches DISPLAY */
Window Root;      /* Hintergrund-Drawable */
int screen;

double Mark_Pos=M_PI/2;
int Pos=0;

int d_depth;
XSizeHints mysizehints;
XWMHints mywmhints;
Pixel back_pix, fore_pix;
GC NormalGC;
Window iconwin, win;       /* My home is my window */
char *ProgName;
char *Geometry;
char Execute[] = "echo no program has been specified >/dev/console";
char *ERR_colorcells = "not enough free color cells\n";

Atom wm_delete_window;
Atom wm_protocols;

/* XPM Variablen *************************************************************/
typedef struct _XpmIcon {
    Pixmap pixmap;
    Pixmap mask;
    XpmAttributes attributes;
}        XpmIcon;

XpmIcon asMix,  Mark;
time_t actualtime; 

/* lokale Funktionen *********************************************************/
#define MW_EVENTS   (ExposureMask | ButtonPressMask | StructureNotifyMask | \
                      ButtonMotionMask | PointerMotionMask)
#define FALSE 0
void GetXPM(void);
Pixel GetColor(char *name);
void RedrawWindow( XpmIcon *v);

/*****************************************************************************/
/*****************************************************************************/
static char *help_message[] = {
"where options include:",
"    -exe <program>          program to start on click",
"    -geometry [+|-]x[+|-]y  position of asmix",
"    -shape                  without groundplate",
"    -iconic                 start up as icon",
"    -withdrawn              start up in withdrawn mode",
"    -channel <channel-id>   specify the channel to control (def: volume)",
"    -device <mixer-device>  specify the mixer device (def: /dev/mixer)",
"    -name <window-name>     specify the name of the window (def: asmix)",
"    -V                      version control",
NULL
};

void version()
{
	printf("asmix: AfterStep sound mixer volume control 1.5\n");
}

void usage()
{
  char **cpp;

  printf("usage:  %s [-options ...] \n", ProgName);
  for (cpp = help_message; *cpp; cpp++) {
    printf("%s\n", *cpp);
  }
  printf("\n");
  exit(1);
}

void UpdatePos(double a,int flag)
{
   XCopyArea(dpy,asMix.pixmap,win,NormalGC,
     Center_X+Radius*cos(Mark_Pos)-Mark.attributes.width/2,
     Center_Y+Radius*sin(Mark_Pos)-Mark.attributes.height/2,
     Mark.attributes.width, Mark.attributes.height,
     Center_X+Radius*cos(Mark_Pos)-Mark.attributes.width/2,
     Center_Y+Radius*sin(Mark_Pos)-Mark.attributes.height/2);
   XCopyArea(dpy,asMix.pixmap,iconwin,NormalGC,
     Center_X+Radius*cos(Mark_Pos)-Mark.attributes.width/2,
     Center_Y+Radius*sin(Mark_Pos)-Mark.attributes.height/2,
     Mark.attributes.width, Mark.attributes.height,
     Center_X+Radius*cos(Mark_Pos)-Mark.attributes.width/2,
     Center_Y+Radius*sin(Mark_Pos)-Mark.attributes.height/2);

   if(flag==1){Mark_Pos=a;flag=0;}
      else Mark_Pos=a;

  Pos=Master.volume.left=Master.volume.right=(unsigned char)aToVol(Mark_Pos);

/*   fprintf(stderr,"set_slider: updating mixer %i to %i:%i\n",Master.mixer_id,Master.volume.left,Master.volume.right);
*/   
  if (ioctl(mixer_fd,MIXER_WRITE(Master.mixer_id),&Master.volume) == -1)
        fprintf(stderr,"Error writing mixer in Handle_slider");
 
   XCopyArea(dpy,Mark.pixmap,win,NormalGC,
     0,0,Mark.attributes.width, Mark.attributes.height,
     Center_X+Radius*cos(Mark_Pos)-Mark.attributes.width/2,
     Center_Y+Radius*sin(Mark_Pos)-Mark.attributes.height/2);
   XCopyArea(dpy,Mark.pixmap,iconwin,NormalGC,
     0,0,Mark.attributes.width, Mark.attributes.height,
     Center_X+Radius*cos(Mark_Pos)-Mark.attributes.width/2,
     Center_Y+Radius*sin(Mark_Pos)-Mark.attributes.height/2);
}

int aToVol(double a)
{
  
  int retval;
  
  if ((a<0) || (a<M_PI/2)) a+=2*M_PI;
  
  a-=3*M_PI/4;
  
  retval=(int)(100*a/(1.5*M_PI));
  if (retval<0) retval=0; else if (retval>100) retval=100;
  
  return retval; 
}

double VolToa(int p)
{
  double NewPos;
  
  NewPos=(float)p/100*3*M_PI/2+3*M_PI/4;
  if ((NewPos>M_PI)&&(NewPos<2*M_PI)) NewPos-=2*M_PI;

  return NewPos;
}

void MouseMove(int x, int y)
{
   double X,Y;
   double a;

   X= (double)x - Center_X;
   Y= (double)y - Center_Y;

   if (sqrt(X*X+Y*Y)<=2) return;
                
   a=atan2(Y,X);

   if ((a<min_a) &&(a>max_a)) {
      if ((Mark_Pos<-M_PI/2)||(Mark_Pos>M_PI/2)) a=min_a; else a=max_a;
   }

   UpdatePos(a,0);
}


static void sync_Control(VolumeControl *vcptr)
{
  int portion;

  /*if (!vcptr->supported)
    return;*/
  if (ioctl(mixer_fd,MIXER_READ(vcptr->mixer_id),&vcptr->volume) == -1)
    perror("Error reading volumes in sync_slider");
  
  portion=(vcptr->volume.left+vcptr->volume.right)/2;
  
/*  fprintf(stderr,"%f : %i : %i\n",NewPos,Pos,portion);*/
  
  if (Pos!=portion) {
     UpdatePos(VolToa(portion),0);
     Pos=portion;
  }
}


int main(int argc,char *argv[])
{
  int i;
  unsigned int borderwidth ;
  char *display_name = NULL; 
  char *wname = "asmix";
  char *mixer_device = "/dev/mixer";
  XGCValues gcv;
  unsigned long gcm;
  XEvent Event;
  XTextProperty name;
  XClassHint classHint;
  Pixmap pixmask;
  int status;

  ProgName = argv[0];
  Geometry = "";


  Master.mixer_id  = SOUND_MIXER_VOLUME;
  Master.supported = SOUND_MIXER_VOLUME;

  /* Parse command line options */
  ProgName = argv[0];

  for(i=1;i<argc;i++) {
    char *arg= argv[i];

    if (arg[0] == '-') {
      switch(arg[1]) {
      case 'e':
	if(++i >=argc) usage();
	strcpy(&Execute[0], argv[i]);
	strcat(&Execute[0], " &");
	continue;
      case 's':
	ONLYSHAPE=1;
	continue;
      case 'g':
	if(++i >=argc) usage();
	Geometry = argv[i];
	continue;
      case 'i':
	ICONIFIED=1;
	continue;
      case 'w':
	WITHDRAWN=1;
	continue;
      case 'c':
	if(++i >=argc) usage();
	{
	  int cur=0;
	  while(1)
	  {
	    if ( channel_idents[cur].channel_name == NULL )
	      usage();
	    if ( strcmp( channel_idents[cur].channel_name, argv[i] ) != 0 )
	    {
	      cur++;
	      continue;
	    }
	    Master.mixer_id  = channel_idents[cur].channel_id;
	    Master.supported = channel_idents[cur].channel_id;
	    break;
	  }
	}
	continue;
      case 'd':
	if(++i >=argc) usage();
	mixer_device=argv[i];
	continue;
      case 'n':
	if(++i >=argc) usage();
	wname=argv[i];
	continue;
      case 'V':
	version();
	exit(0);
      default:
	version();
	usage();
      }
    }
  }

  mixer_fd = open (mixer_device, O_RDWR, 0);   
  if (mixer_fd < 0) {
    fprintf (stderr,"asmix: Error opening mixer device %s", mixer_device);
    exit (1);
  }

  if (ioctl(mixer_fd, SOUND_MIXER_READ_DEVMASK, &Master.supported) == -1)
    Master.supported = 0xffff; /* Assume all are supported */
/*  printf("Master.supported = 0x%x\n",Master.supported);*/


  /* Open the display */
  if (!(dpy = XOpenDisplay(display_name)))  
    { 
      fprintf(stderr,"asmix: can't open display %s\n", 
	      XDisplayName(display_name)); 
      exit (1); 
    } 
  screen= DefaultScreen(dpy);
  Root = RootWindow(dpy, screen);
  d_depth = DefaultDepth(dpy, screen);
  
  /* Icon Daten nach XImage konvertieren */
  GetXPM();
  
  /* Create a window to hold the banner */
  mysizehints.flags= USSize|USPosition;
  mysizehints.x = 0;
  mysizehints.y = 0;

  back_pix = GetColor("grey");
  fore_pix = GetColor("darkgrey");

  XWMGeometry(dpy, screen, Geometry, NULL, (borderwidth =1), &mysizehints,
	      &mysizehints.x,&mysizehints.y,&mysizehints.width,&mysizehints.height, &i); 

  mysizehints.width = asMix.attributes.width;
  mysizehints.height= asMix.attributes.height;

  win = XCreateSimpleWindow(dpy,Root,mysizehints.x,mysizehints.y,
			    mysizehints.width,mysizehints.height,
			    borderwidth,fore_pix,back_pix);
  iconwin = XCreateSimpleWindow(dpy,win,mysizehints.x,mysizehints.y,
				mysizehints.width,mysizehints.height,
				borderwidth,fore_pix,back_pix);


        /* Set up the event for quitting the window */
        wm_delete_window = XInternAtom(
                dpy, 
                "WM_DELETE_WINDOW",     /* atom_name */
                False                   /* only_if_exists */
                );
        wm_protocols = XInternAtom(
                dpy,
                "WM_PROTOCOLS",         /* atom_name */
                False                   /* only_if_exists */
                );
        status = XSetWMProtocols(
                dpy, 
                win,
                &wm_delete_window,
                1
                );
        status = XSetWMProtocols(
                dpy, 
                iconwin,
                &wm_delete_window,
                1
                );
                

  /* Hints aktivieren */
  XSetWMNormalHints(dpy, win, &mysizehints);
  classHint.res_name =  "asmix";
  classHint.res_class = "asMix";
  XSetClassHint(dpy, win, &classHint);

  XSelectInput(dpy, win, MW_EVENTS);
  XSelectInput(dpy, iconwin, MW_EVENTS);
  
  if (XStringListToTextProperty(&wname, 1, &name) ==0) {
    fprintf(stderr, "asmix: can't allocate window name [%s]\n", wname);
    exit(-1);
  }
  XSetWMName(dpy, win, &name);
  XSetWMName(dpy, iconwin, &name);
  
  /* Create a GC for drawing */
  gcm = GCForeground|GCBackground|GCGraphicsExposures;
  gcv.foreground = fore_pix;
  gcv.background = back_pix;
  gcv.graphics_exposures = FALSE;
  NormalGC = XCreateGC(dpy, Root, gcm, &gcv);  

  if (ONLYSHAPE) {
/* try to make shaped window here */
/*    pixmask = XCreateBitmapFromData(dpy, win, mask_bits,
				    mask_width, mask_height);*/
    XShapeCombineMask(dpy, win, ShapeBounding, 0, 0, asMix.mask, ShapeSet);
    XShapeCombineMask(dpy, iconwin, ShapeBounding, 0, 0, asMix.mask, ShapeSet);
  }
 
  mywmhints.initial_state = 
	  WITHDRAWN ? WithdrawnState : 
	  ICONIFIED ? IconicState : NormalState;
  mywmhints.window_group = win;
  mywmhints.flags = StateHint | IconWindowHint | IconPositionHint | WindowGroupHint;
  mywmhints.icon_window = iconwin;
  mywmhints.icon_x = mysizehints.x;
  mywmhints.icon_y = mysizehints.y;
  XSetWMHints(dpy, win, &mywmhints); 

  status = XSetCommand(dpy, win, argv, argc);
  
  XSetWindowBackgroundPixmap(dpy,win,asMix.pixmap);
  XSetWindowBackgroundPixmap(dpy,iconwin,asMix.pixmap);

  sync_Control(&Master); 

  XMapWindow(dpy,win);

  RedrawWindow(&asMix);
  while(1)
    {
      if (actualtime != time(0))
	{
	  actualtime = time(0);
	  
          sync_Control(&Master); 
	  
	}
      
      /* read a packet */
      while (XPending(dpy))
	{
	  XNextEvent(dpy,&Event);
	  switch(Event.type)
	    {
                case ClientMessage:
                        if ((Event.xclient.message_type == wm_protocols)
                          && (Event.xclient.data.l[0] == wm_delete_window))
			{
			      XCloseDisplay(dpy);
			      exit(0);
			}
                        break;
	    case Expose:
	      if(Event.xexpose.count == 0 )
		RedrawWindow(&asMix);
	      break;
	    case ButtonPress:
              if (Event.xbutton.button == Button1) {
                  MouseMove(Event.xbutton.x, Event.xbutton.y);
               } else if (Event.xbutton.button == Button2) {
	           system(Execute);
	       }
                 else if (Event.xbutton.button == Button4) {
		   UpdatePos(WIZ,1);
	       }
	         else if (Event.xbutton.button == Button4) {                    
		   UpdatePos(-WIZ,1);	  
	       }
	      break;
	    case  MotionNotify: {
	        Window Root, Child;
	        int root_x, root_y;
	        int win_x, win_y;
	        unsigned int mask;
	       
                if (XQueryPointer(dpy, win, &Root, &Child,
                    &root_x, &root_y, &win_x, &win_y, &mask)!=0) {
                   if (mask & Button1MotionMask) 
                      MouseMove(Event.xbutton.x, Event.xbutton.y);
                 }
	        break;
	      }
	    case DestroyNotify:
              XFreeGC(dpy, NormalGC);
      XFlush(dpy);
/*              fprintf(stderr,"DestroyMe?\n");*/
/*              XDestroyWindow(dpy, win);
	      XDestroyWindow(dpy, iconwin);*/
              XCloseDisplay(dpy);
	      exit(0);
	    default:
	      break;      
	    }
	}
#ifdef SYSV
      poll((struct poll *) 0, (size_t) 0, 50);
#else
      usleep(50000L);			/* 50/100 sec */
#endif
    }
  return 0;
}
/****************************************************************************/
void nocolor(char *a, char *b)
{
 fprintf(stderr,"asmix: can't %s %s\n", a,b);
}
/****************************************************************************/
/* Konvertiere XPMIcons nach XImage */
void GetXPM(void)
{
  static char **bg_xpm;
  XColor col;
  XWindowAttributes attributes;
  int ret;

  bg_xpm =ONLYSHAPE ? volume_xpm : volume_xpm;

  /* for the colormap */
  XGetWindowAttributes(dpy,Root,&attributes);

  asMix.attributes.valuemask |= (XpmReturnPixels | XpmReturnExtensions);
  ret = XpmCreatePixmapFromData(dpy, Root, bg_xpm, &asMix.pixmap, 
				&asMix.mask, &asMix.attributes);
  if(ret != XpmSuccess)
    {fprintf(stderr, ERR_colorcells);exit(1);}


  Mark.attributes.valuemask |= (XpmReturnPixels | XpmReturnExtensions);
  ret = XpmCreatePixmapFromData(dpy, Root, mark_xpm, &Mark.pixmap, 
				&Mark.mask, &Mark.attributes);
  if(ret != XpmSuccess)
    {fprintf(stderr, ERR_colorcells);exit(1);}

}
/****************************************************************************/
/* Removes expose events for a specific window from the queue */
int flush_expose (Window w)
{
  XEvent dummy;
  int i=0;
  
  while (XCheckTypedWindowEvent (dpy, w, Expose, &dummy))i++;
  return i;
}

/****************************************************************************/
/* Draws the icon window */
void RedrawWindow( XpmIcon *v)
{
  flush_expose (iconwin);
  
    XCopyArea(dpy,Mark.pixmap,win,NormalGC,
              0,0,Mark.attributes.width, Mark.attributes.height,
              Center_X+Radius*cos(Mark_Pos)-Mark.attributes.width/2,
              Center_Y+Radius*sin(Mark_Pos)-Mark.attributes.height/2);

   XCopyArea(dpy,Mark.pixmap,iconwin,NormalGC,
              0,0,Mark.attributes.width, Mark.attributes.height,
              Center_X+Radius*cos(Mark_Pos)-Mark.attributes.width/2,
              Center_Y+Radius*sin(Mark_Pos)-Mark.attributes.height/2);

}
/****************************************************************************/
Pixel GetColor(char *name)
{
  XColor color;
  XWindowAttributes attributes;

  XGetWindowAttributes(dpy,Root,&attributes);
  color.pixel = 0;
   if (!XParseColor (dpy, attributes.colormap, name, &color)) 
     {
       nocolor("parse",name);
     }
   else if(!XAllocColor (dpy, attributes.colormap, &color)) 
     {
       nocolor("alloc",name);
     }
  return color.pixel;
}
